/*
 * Copyright (C) 2006-2007 Eskil Bylund
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 */

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.Text;

using Gtk;
using Glade;
using Mono.Unix;

using DCSharp.Backend.Connections;
using DCSharp.Backend.Objects;
using DCSharp.Backend.Managers;

namespace DCSharp.Gui
{
	public class HubPage : Page
	{
		private HubConnection hub;

		private ActionGroup actionGroup;

		// Users
		private UserStore userStore;
		private List<Identity> addQueue;
		private List<Identity> removeQueue;
		private uint idleAddTimeout;
		private uint idleRemoveTimeout;
		private uint searchTimeoutId;

		private SortType sortOrder;
		private int sortColumnId;

		// Chat
		private TextMark helperStartMark;
		private TextMark helperEndMark;
		private Widget separator;

		private TextTag nickTag;

		private bool showSeparator;

		private bool hovering;
		private static Gdk.Cursor handCursor;
		private static Gdk.Cursor normalCursor;

		[Widget]
		private Box mainBox;

		[Widget]
		private HPaned hpaned;

		[Widget]
		private ChatView chatView;

		[Widget]
		private Entry messageEntry;

		[Widget]
		private Entry searchEntry;

		[Widget]
		private UserView userView;

		#region Constructors

		static HubPage()
		{
			handCursor = new Gdk.Cursor(Gdk.CursorType.Hand2);
			normalCursor = new Gdk.Cursor(Gdk.CursorType.Xterm);
		}

		public HubPage(HubConnection hub) : base("HubPage.glade")
		{
			if (hub == null)
			{
				throw new ArgumentNullException("hub");
			}

			addQueue = new List<Identity>();
			removeQueue = new List<Identity>();

			// User interface
			ActionEntry[] entries = new ActionEntry[] {
				new ActionEntry("VisibleColumns", null, Catalog.GetString("_Visible Columns..."), null,
					Catalog.GetString("Edit visible columns"),
					null),
			};

			actionGroup = new ActionGroup("DownloadPageActions");
			actionGroup.Add(entries);

			// Widgets
			nickTag = chatView.Buffer.TagTable.Lookup("nick");

			TextTag tag = new TextTag("separator");
			tag.Justification = Justification.Center;
			chatView.Buffer.TagTable.Add(tag);

			chatView.MotionNotifyEvent += OnChatViewMotionNotifyEvent;
			chatView.WidgetEvent += OnChatViewWidgetEvent;
			chatView.Show();

			// User view
			userStore = new UserStore();
			userStore.SetSortFunc((int)UserStore.Column.Nick, UserSortFunc);
			userStore.SetSortFunc((int)UserStore.Column.ShareSize, SortByShareSize);
			userStore.SetSortColumnId((int)UserStore.Column.Nick,
				SortType.Ascending);

			userView.Model = userStore;
			userView.SetSizeRequest(140, 0);
			userView.Show();

			userView.GetColumn(1).Visible = false;

			// Visible columns
			VisibleColumnsWindow.ConnectToWidget(this, actionGroup["VisibleColumns"],
				userView.Columns, userView.Columns[0]);

			// Message entry
			messageEntry = new UserEntry();

			// Modifying a TreeModel connected to a EntryCompletion can be
			// incredibly slow!
			messageEntry.FocusInEvent += delegate
			{
				messageEntry.Completion.Model = userStore;
			};
			messageEntry.FocusOutEvent += delegate
			{
				messageEntry.Completion.Model = null;
			};
			messageEntry.Activated += OnMessageEntryActivated;
			messageEntry.Show();
			messageEntry.GrabFocus();

			mainBox.PackEnd(messageEntry, false, false, 0);

			// Search entry
			searchEntry.Changed += OnSearchEntryChanged;
			searchEntry.Show();

			Hub = hub;
		}

		#endregion

		#region Classes

		private class UserEntry : Entry
		{
			public UserEntry()
			{
				Completion = new NickEntryCompletion();

				Completion.TextColumn = (int)UserStore.Column.Nick;
			}

			// This is the same hack as the GtkFileChooserEntry uses to keep
			// the focus in the entry when pressing tab.
			protected override bool OnFocused(DirectionType direction)
			{
				bool controlPressed = false;
				Gdk.ModifierType state;

				if (Gtk.Global.GetCurrentEventState(out state))
				{
					controlPressed = (state & Gdk.ModifierType.ControlMask) ==
						Gdk.ModifierType.ControlMask;
				}

				if (direction == DirectionType.TabForward && HasFocus &&
					!controlPressed)
				{
					Position = -1;

					return true;
				}
				return base.OnFocused(direction);
			}
		}

		private class NickEntryCompletion : EntryCompletion
		{
			public NickEntryCompletion()
			{
				InlineCompletion = true;
				PopupCompletion = true;
			}

			protected override bool OnMatchSelected(TreeModel model,
				TreeIter iter)
			{
				Entry entry = Entry as Entry;

				if (TextColumn >= 0)
				{
					string nick = model.GetValue(iter, TextColumn) as string;

					entry.Text = nick + ": ";
					entry.Position = -1;
					return true;
				}
				return false;
			}
		}

		#endregion

		#region Properties

		public override string Title
		{
			get { return Util.GetHubName(hub); }
		}

		public override Gdk.Pixbuf Icon
		{
			get { return Util.GetHubIcon(hub); }
		}

		public override string Tooltip
		{
			get
			{
				string title = Util.GetHubName(hub);
				if (title != hub.Uri.ToString())
				{
					title += " (" + hub.Uri + ")";
				}
				if (hub.Name != null)
				{
					title += "\n";
					title += hub.Name;
				}
				return title;
			}
		}

		public override ActionGroup Actions
		{
			get { return actionGroup; }
		}

		public int PanePosition
		{
			get { return hpaned.Position; }
			set { hpaned.Position = value; }
		}

		public int VisibleColumns
		{
			get { return Util.GetVisibleColumns(userView.Columns); }
			set { Util.SetVisibleColumns(userView.Columns, value | 1); }
		}

		/// <summary>
		/// Gets or sets whether or not to show the new message separator on a
		///  new message.
		/// <summary>
		public bool ShowSeparatorOnMessage
		{
			get { return showSeparator; }
			set { showSeparator = value; }
		}

		public HubConnection Hub
		{
			get { return hub; }
			set
			{
				if (hub != null)
				{
					hub.Message -= OnHubMessage;
					hub.NameChanged -= OnHubNameChanged;
					hub.StateChanged -= OnHubStateChanged;
					hub.Error -= OnHubError;

					hub.Users.UserOnline -= OnUserOnline;
					hub.Users.UserOffline -= OnUserOffline;
					hub.Users.UserChanged -= OnUserChanged;
				}

				hub = value;
				if (hub != null)
				{
					hub.Message += OnHubMessage;
					hub.NameChanged += OnHubNameChanged;
					hub.StateChanged += OnHubStateChanged;
					hub.Error += OnHubError;

					hub.Users.UserOnline += OnUserOnline;
					hub.Users.UserOffline += OnUserOffline;
					hub.Users.UserChanged += OnUserChanged; 
				}
			}
		}

		#endregion

		#region Methods

		/// <summary>
		/// Updates the position of the new message separator, moving it to the
		/// end of the chat view.
		/// </summary>
		public void UpdateNewMessageSeparator()
		{
			// TODO: Move code to the ChatView?
			TextIter startIter;
			TextIter endIter;
			TextBuffer buffer = chatView.Buffer;

			// Remove the previous anchor
			if (helperStartMark != null && helperEndMark != null)
			{
				startIter = chatView.Buffer.GetIterAtMark(helperStartMark);
				endIter = chatView.Buffer.GetIterAtMark(helperEndMark);

				chatView.Remove(separator);
				separator.Hide();

				buffer.Delete(ref startIter, ref endIter);
				buffer.DeleteMark(helperStartMark);
				buffer.DeleteMark(helperEndMark);
			}

			// Create a new anchor
			endIter = chatView.Buffer.EndIter;
			helperStartMark = buffer.CreateMark(null, endIter, true);

			TextChildAnchor anchor = buffer.CreateChildAnchor(ref endIter);
			buffer.Insert(ref endIter, "\n");

			startIter = buffer.GetIterAtMark(helperStartMark);
			buffer.ApplyTag("separator", startIter, endIter);

			helperEndMark = buffer.CreateMark(null, endIter, true);

			// Attach the separator to the anchor
			if (separator == null)
			{
				separator = new HSeparator();
				separator.SetSizeRequest(200, -1);
				separator.Destroyed += delegate
				{
					separator = null;
				};
			}
			chatView.AddChildAtAnchor(separator, anchor);
		}

		protected override void OnDestroyed()
		{
			if (searchTimeoutId != 0)
			{
				GLib.Source.Remove(searchTimeoutId);
				searchTimeoutId = 0;
			}
			if (idleAddTimeout != 0)
			{
				GLib.Source.Remove(idleAddTimeout);
				idleAddTimeout = 0;
			}
			if (idleRemoveTimeout != 0)
			{
				GLib.Source.Remove(idleRemoveTimeout);
				idleRemoveTimeout = 0;
			}

			HubConnection hub = this.hub;
			if (hub != null)
			{
				// Remove the event handlers
				Hub = null;

				hub.Disconnect();
			}
			base.OnDestroyed();
		}

		protected override bool OnKeyPressEvent(Gdk.EventKey evnt)
		{
			if (evnt.Key == Gdk.Key.Escape && hub != null &&
				hub.State == ConnectionState.Connecting)
			{
				hub.Disconnect();
				return true;
			}
			return base.OnKeyPressEvent(evnt);
		}

		protected void DisplayMessage(Identity user, string message)
		{
			// Display the new message separator
			if (showSeparator && separator != null)
			{
				separator.Show();
			}

			if (user != null)
			{
				bool self = user.Nick == hub.LocalIdentity.Nick;
				chatView.InsertMessage(user.Nick, message, self);
			}
			else
			{
				chatView.InsertText(message);
			}
			OnPageChanged();
		}

		protected void DisplayState(ConnectionState state)
		{
			string message;

			switch (state)
			{
				case ConnectionState.Connected:
					IsWorking = false;
					message = Catalog.GetString("Connected");
					break;

				case ConnectionState.Disconnected:
					IsWorking = false;
					message = Catalog.GetString("Disconnected");
					break;

				default:
					IsWorking = true;
					message = String.Format(
						Catalog.GetString("Connecting to {0}..."), hub.Uri);
					break;
			}

			chatView.InsertStatus(message);
			OnPageChanged();

			Status = message;
		}

		protected void DisplayError(ConnectionError error)
		{
			string message;

			switch (error)
			{
				case ConnectionError.Dns:
					message = String.Format(
						Catalog.GetString("Could not resolve the address {0}"),
						hub.Uri);
					break;

				case ConnectionError.Timeout:
					message = Catalog.GetString("The connection timed out");
					break;

				default:
					// FIXME: Should be "Could not connect to {0}".
					message = Catalog.GetString("There was an error connecting");
					break;
			}

			chatView.InsertError(message);
			OnPageChanged();
		}

		protected void UpdateStatus()
		{
			if (hub != null && hub.State == ConnectionState.Connected)
			{
				int count = hub.Users.Count;
				string status = Catalog.GetPluralString("{0} user online",
					"{0} users online", count);
				status = String.Format(status, count);

				Status = Catalog.GetString("Connected") + ". " + status;
			}
		}

		#region User Loading

		private void ReloadUsers()
		{
			userStore.Clear();
			DisableSorting();

			lock (hub.Users.SyncRoot)
			{
				foreach (Identity identity in hub.Users)
				{
					if (MatchesFilter(identity))
					{
						userStore.AddUser(identity, hub);
					}
				}
				addQueue.Clear();
				removeQueue.Clear();
			}

			RestoreSorting();
			UpdateStatus();
		}

		private bool MatchesFilter(Identity identity)
		{
			string searchText = searchEntry.Text;
			return searchText == null || CultureInfo.CurrentCulture.CompareInfo.IndexOf(
				identity.Nick, searchText, CompareOptions.IgnoreCase) >= 0;
		}

		private void IdleAdd(Identity identity)
		{
			lock (addQueue)
			{
				addQueue.Add(identity);
				removeQueue.Remove(identity);

				if (idleAddTimeout == 0)
				{
					idleAddTimeout = GLib.Timeout.Add(1500, AddUsersInQueue);
				}
			}
		}

		private void IdleRemove(Identity identity)
		{
			lock (addQueue)
			{
				removeQueue.Add(identity);
				addQueue.Remove(identity);

				if (idleRemoveTimeout == 0)
				{
					idleRemoveTimeout = GLib.Idle.Add(RemoveUsersInQueue);
				}
			}
		}

		// FIXME: Loading the users still blocks the gui.
		private bool AddUsersInQueue()
		{
			// Reload if there are many pending users since sorting is disabled
			// when reloading.
			if (addQueue.Count > 10)
			{
				ReloadUsers();
				lock (addQueue)
				{
					if (addQueue.Count > 0)
					{
						return true;
					}
					idleAddTimeout = 0;
					return false;
				}
			}
			lock (addQueue)
			{
				while (addQueue.Count > 0 && hub.State == ConnectionState.Connected)
				{
					Identity identity = addQueue[0];
					addQueue.RemoveAt(0);

					if (MatchesFilter(identity))
					{
						userStore.AddUser(identity, hub);
					}
				}
			}
			UpdateStatus();

			idleAddTimeout = 0;
			return false;
		}

		private bool RemoveUsersInQueue()
		{
			lock (addQueue)
			{
				while (removeQueue.Count > 0 && hub.State == ConnectionState.Connected)
				{
					Identity identity = removeQueue[0];
					removeQueue.Remove(identity);

					userStore.RemoveUser(identity);
				}
			}
			UpdateStatus();

			idleRemoveTimeout = 0;
			return false;
		}

		private void DisableSorting()
		{
			if (userStore.GetSortColumnId(out sortColumnId, out sortOrder))
			{
				userStore.SetSortColumnId(-2, SortType.Ascending);
			}
		}

		private void RestoreSorting()
		{
			userStore.SetSortColumnId(sortColumnId, sortOrder);
		}

		#endregion

		#region User Sorting

		private int UserSortFunc(TreeModel model, TreeIter tia, TreeIter tib)
		{
			Identity userA = model.GetValue(tia, (int)UserStore.Column.User) as Identity;
			Identity userB = model.GetValue(tib, (int)UserStore.Column.User) as Identity;

			if (userA != null && userB != null)
			{
				// Put operators before regular users
				if (userA.Op && !userB.Op)
				{
					return -1;
				}
				if (!userA.Op && userB.Op)
				{
					return 1;
				}
				return String.Compare(userA.Nick, userB.Nick);
			}

			// Fallback
			string namea = model.GetValue(tia, (int)UserStore.Column.Nick) as string;
			string nameb = model.GetValue(tib, (int)UserStore.Column.Nick) as string;

			return String.Compare(namea, nameb);
		}

		private int SortByShareSize(TreeModel model, TreeIter a, TreeIter b)
		{
			Identity userA = (Identity)model.GetValue(a,
				(int)UserStore.Column.User);
			Identity userB = (Identity)model.GetValue(b,
				(int)UserStore.Column.User);

			return userA.ShareSize.CompareTo(userB.ShareSize);
		}

		#endregion

		private Identity GetUser(string nick)
		{
			if (hub == null)
			{
				return null;
			}
			return hub.Users.Find(delegate(Identity user)
			{
				return user.Nick == nick;
			});
		}

		private void OnChatViewWidgetEvent(object obj, WidgetEventArgs args)
		{
			Gdk.EventButton evnt = args.Event as Gdk.EventButton;
			if (evnt == null || evnt.Type != Gdk.EventType.ButtonPress ||
				evnt.Button != 1)
			{
				return;
			}

			int x, y;
			chatView.WindowToBufferCoords(TextWindowType.Text, (int)evnt.X,
				(int)evnt.Y, out x, out y);

			TextIter iter = chatView.GetIterAtLocation(x, y);
			TextIter end = iter;

			if (iter.HasTag(nickTag) && iter.BackwardToTagToggle(nickTag) &&
				end.ForwardToTagToggle(nickTag))
			{
				string nick = iter.GetText(end);
				nick = nick.Substring(0, nick.IndexOf(":"));

				Identity user = GetUser(nick);
				TreeIter treeIter;
				if (user != null && userStore.GetIter(user, out treeIter))
				{
					userView.Selection.SelectIter(treeIter);

					TreePath path = userView.Model.GetPath(treeIter);
					userView.ScrollToCell(path, userView.Columns[0], true,
						0.5f, 0f);
				}
				else
				{
					userView.Selection.UnselectAll();
				}
			}
		}

		private void OnChatViewMotionNotifyEvent(object obj,
			MotionNotifyEventArgs args)
		{
			int pointerX, pointerY;
			Gdk.ModifierType pointerMask;

			Gdk.Window window = chatView.GetWindow(TextWindowType.Text);
			window.GetPointer(out pointerX, out pointerY, out pointerMask);

			int x, y;
			chatView.WindowToBufferCoords(TextWindowType.Widget, pointerX,
				pointerY, out x, out y);

			TextIter iter = chatView.GetIterAtLocation(x, y);
			bool hovering = iter.HasTag(nickTag);

			if (hovering != this.hovering)
			{
				this.hovering = hovering;
				if (this.hovering)
				{
					window.Cursor = handCursor;
				}
				else
				{
					window.Cursor = normalCursor;
				}
			}
		}

		private void OnMessageEntryActivated(object obj, EventArgs args)
		{
			string message = messageEntry.Text.Trim();
			if (hub != null && message.Length > 0)
			{
				try
				{
					hub.SendMessage(message);
					messageEntry.Text = String.Empty;
				}
				catch
				{
					chatView.InsertError(
						Catalog.GetString("Could not send the message to the hub."));
				}
			}
		}

		private void OnSearchEntryChanged(object obj, EventArgs args)
		{
			if (searchTimeoutId > 0)
			{
				GLib.Source.Remove(searchTimeoutId);
			}

			searchTimeoutId = GLib.Timeout.Add(300, delegate
			{
				ReloadUsers();
				return false;
			});
		}

		private void OnHubMessage(object obj, MessageEventArgs args)
		{
			Application.Invoke(delegate
			{
				DisplayMessage(args.From, args.Message);
			});
		}

		private void OnHubNameChanged(object obj, EventArgs args)
		{
			string name = ((HubConnection)obj).Name;

			Application.Invoke(delegate
			{
				string status = String.Format(
					Catalog.GetString("The topic is: {0}"), name);

				chatView.InsertStatus(status);
				OnPageChanged();
			});
		}

		private void OnHubStateChanged(object obj, EventArgs args)
		{
			ConnectionState state = hub.State;
			Application.Invoke(delegate
			{
				if (state == ConnectionState.Connected)
				{
					messageEntry.Sensitive = true;
				}
				else if (state == ConnectionState.Disconnected)
				{
					lock (addQueue)
					{
						addQueue.Clear();
						removeQueue.Clear();
					}
					userStore.Clear();

					messageEntry.Sensitive = false;
				}
				DisplayState(state);
			});
		}

		private void OnHubError(object obj, ConnectionErrorEventArgs args)
		{
			Application.Invoke(delegate
			{
				DisplayError(args.Error);
			});
		}

		private void OnUserOnline(object obj, IdentityEventArgs args)
		{
			IdleAdd(args.Identity);
		}

		private void OnUserOffline(object obj, IdentityEventArgs args)
		{
			IdleRemove(args.Identity);
		}

		private void OnUserChanged(object obj, IdentityEventArgs args)
		{
			// TODO: Update the store if the nick changes.
		}

		#endregion
	}
}
